/*
 * Copyright 2012-2013 Stephane Godbillon (@sgodbillon) and Zenexity
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package reactivemongo

import java.io.InputStream

import java.net.URI

import scala.util.control.NonFatal

import scala.collection.immutable.ListSet

import scala.concurrent.{ ExecutionContext, Future }

import reactivemongo.core.errors.GenericDriverException

package object util extends UtilCompat {
  private val HEX_CHARS: Array[Char] = "0123456789abcdef".toCharArray

  /** Turns an array of Byte into a String representation in hexadecimal. */
  private[reactivemongo] def hex2Str(bytes: Array[Byte]): String = {
    val hex = new Array[Char](2 * bytes.length)
    var i = 0
    while (i < bytes.length) {
      hex(2 * i) = HEX_CHARS((bytes(i) & 0xf0) >>> 4)
      hex(2 * i + 1) = HEX_CHARS(bytes(i) & 0x0f)
      i = i + 1
    }
    new String(hex)
  }

  /**
   * Returns the MD5 hash for the given `string`,
   * and turns it into a hexadecimal String representation.
   *
   * @param string the string to be hashed
   * @param encoding the string encoding/charset
   */
  private[reactivemongo] def md5Hex(string: String, encoding: String): String =
    hex2Str(md5(string, encoding))

  /**
   * Returns the MD5 hash of the given `string`.
   *
   * @param string the string to be hashed
   * @param encoding the string encoding/charset
   */
  private def md5(string: String, encoding: String): Array[Byte] =
    md5(string.getBytes(encoding))

  /** Computes the MD5 hash of the given `bytes`. */
  private def md5(bytes: Array[Byte]): Array[Byte] =
    java.security.MessageDigest.getInstance("MD5").digest(bytes)

  /** Makes an option of the value matching the condition. */
  private[reactivemongo] def option[T](
      cond: => Boolean,
      value: => T
    ): Option[T] = if (cond) Some(value) else None

  // ---

  private[reactivemongo] def withContent[T](
      uri: URI
    )(f: InputStream => T
    ): T = {
    lazy val in = if (uri.getScheme == "classpath") {
      Thread
        .currentThread()
        .getContextClassLoader
        .getResourceAsStream(uri.getPath)

    } else {
      uri.toURL.openStream()
    }

    try {
      f(in)
    } catch {
      case NonFatal(cause) => throw cause
    } finally {
      in.close()
    }
  }

  // ---

  import scala.concurrent.duration.FiniteDuration

  import org.xbill.DNS.{ Lookup, Name, Record, Type }

  private[reactivemongo] lazy val dnsTimeout = FiniteDuration(5, "seconds")

  /** Host/address and port */
  type SRV = (String, Int)

  /**
   * @param name the DNS name (e.g. `mycluster.mongodb.com`)
   * @param resolver the record resolver
   */
  private[reactivemongo] def srvRecords(
      name: String
    )(resolver: SRVRecordResolver
    )(implicit
      ec: ExecutionContext
    ): Future[List[(String, Int)]] = {
    val resolve = new DefaultSRVResolver(resolver)

    resolve(name)
  }

  type SRVRecordResolver = ExecutionContext => String => Future[Array[Record]]

  /**
   * @param srvPrefix the SRV prefix (default: `_mongodb._tcp`)
   * @param timeout the resolution timeout (default: 5 seconds)
   */
  private[reactivemongo] def dnsResolve(
      srvPrefix: String = "_mongodb._tcp",
      timeout: FiniteDuration = dnsTimeout
    ): SRVRecordResolver = { implicit ec: ExecutionContext =>
    { (name: String) =>
      val service = Name.fromConstantString(name + '.')

      if (service.labels < 3) {
        Future.failed[Array[Record]](
          new GenericDriverException(
            s"Invalid DNS service name (e.g. 'service.domain.tld'): $service"
          )
        )

      } else
        Future {
          val srvName =
            Name.concatenate(Name.fromConstantString(srvPrefix), service)

          val lookup = new Lookup(srvName, Type.SRV)

          lookup.setResolver {
            val r = Lookup.getDefaultResolver
            r.setTimeout(java.time.Duration ofSeconds timeout.toSeconds)
            r
          }

          lookup.run()
        }
    }
  }

  type TXTResolver = String => Future[ListSet[String]]

  /**
   * @param name the DNS name (e.g. `mycluster.mongodb.com`)
   * @param timeout the resolution timeout (default: 5 seconds)
   */
  def txtRecords(
      timeout: FiniteDuration = dnsTimeout
    )(implicit
      ec: ExecutionContext
    ): TXTResolver = { (name: String) =>
    val lookup = new Lookup(name, Type.TXT)

    lookup.setResolver {
      val r = Lookup.getDefaultResolver
      r.setTimeout(java.time.Duration ofSeconds timeout.toSeconds)
      r
    }

    Future(lookup.run()).map {
      case null => ListSet.empty[String]

      case records => {
        val txts: ListSet[String] = toListSet(records) { rec =>
          val data = rec.rdataToString
          val stripped = data.stripPrefix("\"")

          if (stripped == data) {
            data
          } else {
            stripped.stripSuffix("\"")
          }
        }

        txts
      }
    }
  }
}
